# -*- coding: utf-8 -*-
'''Un acortador de URLs pero que permite:

* Editar adonde apunta el atajo más tarde
* Eliminar atajos
* Definir tests para saber si el atajo es válido

'''

import os
import sys
import string
import datetime
import urlparse

from twill.commands import go, code, find, notfind, title


def minitwill(url, script):
    '''Dada una URL y un script en una versión limitada
    de twill, ejecuta ese script.
    Apenas una línea falla, devuelve False.

    Si todas tienen éxito, devuelve True.

    Ejemplos:

    >>> minitwill('http://google.com','code 200')
    ==> at http://www.google.com.ar/
    True

    >>> minitwill('http://google.com','title bing')
    ==> at http://www.google.com.ar/
    title is 'Google'.
    False

    '''
    try:
        go(url)
    except:
        return False
    for line in script.splitlines():
        cmd, arg = line.split(' ', 1)
        try:
            if cmd in ['code', 'find', 'notfind', 'title']:
                # Si line es "code 200", esto es el equivalente
                # de code(200)
                r = globals()[cmd](arg)
        except:
            return False
    return True


# Usamos storm para almacenar los datos
from storm.locals import *


# FIXME: tengo que hacer más consistentes los nombres
# de los métodos.
class Atajo(object):
    '''Representa una relación slug <=> URL

    Miembros:

    id     = Único, creciente, entero (primary key)
    url    = la URL original
    test   = un test de validez de la URL
    user   = el dueño del atajo
    activo = Si este atajo está activo o no.
             Nunca hay que borrarlos, sino el ID puede volver
             atrás y se "recicla" una URL. ¡Malo, malo, malo!
    status = Resultado del último test (bien/mal)
    ultimo = Fecha/hora del último test
    '''

    # Hacer que los datos se guarden via Storm
    __storm_table__ = "atajo"
    id = Int(primary=True)
    url = Unicode()
    test = Unicode()
    user = Unicode()
    activo = Bool()
    status = Bool()
    ultimo = DateTime()

    def __init__(self, url, user, test=''):
        '''Exigimos la URL y el usuario, test es opcional,
        _id es automático.'''

        # Hace falta crear esto?
        r = self.store.find(Atajo, user=user, url=url)
        self.url = url
        self.user = user
        self.activo = True
        # Test por default, verifica que la página exista.
        self.test = u'code 200'
        if r.count():
            # FIXME: esto creo que es una race condition
            # Existe la misma URL para el mismo usuario,
            # reciclamos el id y el test, pero activa.
            viejo = r.one()
            Atajo.store.remove(viejo)
            self.id = viejo.id
            self.test = viejo.test
        self.store.add(self)
        # Autosave/flush/commit a la base de datos
        self.save()

    def save(self):
        '''Método de conveniencia'''
        Atajo.store.flush()
        Atajo.store.commit()

    @classmethod
    def init_db(cls):
        # Creamos una base SQLite
        if not os.path.exists('pyurl.sqlite'):
            cls.database = create_database(
                "sqlite:///pyurl.sqlite")
            cls.store = Store(cls.database)
            try:
                # Creamos la tabla
                cls.store.execute('''
                CREATE TABLE atajo (
                    id INTEGER PRIMARY KEY,
                    url VARCHAR,
                    test VARCHAR,
                    user VARCHAR,
                    activo TINYINT,
                    status TINYINT,
                    ultimo TIMESTAMP
                )''')
                cls.store.flush()
                cls.store.commit()
            except:
                pass
        else:
            cls.database = create_database(
                "sqlite:///pyurl.sqlite")
            cls.store = Store(cls.database)

    # Caracteres válidos en un atajo de URL
    validos = string.letters + string.digits

    def slug(self):
        '''Devuelve el slug correspondiente al
        ID de este atajo

        Básicamente un slug es un número en base 62,
        representado usando a-zA-Z0-9 como "dígitos",
        y dado vuelta:

        Más significativo a la derecha.

        Ejemplo:

        100000 => '4aA'
        100001 => '5aA'

        '''
        s = ''
        n = self.id
        while n:
            s += self.validos[n % 62]
            n = n // 62
        return s

    @classmethod
    # FIXME: no estoy feliz con esta API
    def get(cls, slug=None, user=None, url=None):
        ''' Dado un slug, devuelve el atajo correspondiente.

            Dado un usuario:
            Si url es None, devuelve la lista de sus atajos
            Si url no es None , devuelve *ese* atajo.
        '''
        if slug is not None:
            i = 0
            for p, l in enumerate(slug):
                i += 62 ** p * cls.validos.index(l)
            return cls.store.find(cls, id=i,
                activo=True).one()
        if user is not None:
            if url is None:
                return cls.store.find(cls, user=user,
                    activo=True)
            else:
                return cls.store.find(cls, user=user,
                    url=url, activo=True).one()

    def delete(self):
        '''Eliminar este objeto de la base de datos'''
        self.activo = False
        self.save()

    def run_test(self):
        '''Correr el test con minitwill y almacenar
        el resultado'''
        self.status = minitwill(self.url, self.test)
        self.ultimo = datetime.datetime.now()
        self.save()

# Usamos bottle para hacer el sitio
import bottle

# Middlewares
from beaker.middleware import SessionMiddleware
from authkit.authenticate import middleware
from paste.auth.auth_tkt import AuthTKTMiddleware


@bottle.route('/logout')
def logout():
    bottle.request.environ['paste.auth_tkt.logout_user']()
    if 'REMOTE_USER' in bottle.request.environ:
        del bottle.request.environ['REMOTE_USER']
    bottle.redirect('/')


@bottle.post('/')
@bottle.get('/')
@bottle.view('usuario.tpl')
def alta():
    """Crea un nuevo slug."""
    # Requerimos que el usuario esté autenticado.
    if not 'REMOTE_USER' in bottle.request.environ:
        bottle.abort(401, "Sorry, access denied.")
    usuario = bottle.request.environ['REMOTE_USER'].decode('utf8')
    # Data va a contener todo lo que el template
    # requiere para hacer la página
    data = {}
    # Esto probablemente debería obtenerse de una
    # configuración
    data['baseurl'] = 'http://pyurl.sytes.net/'
    # Si tenemos un parámetro URL, estamos en esta
    # funcion porque el usuario envió una URL a acortar.
    if 'url' in bottle.request.POST:
        # La acortamos
        url = bottle.request.POST['url'].decode('utf8')
        if not urlparse.urlparse(url).scheme:
            url = 'http://' + url
        parseada = urlparse.urlparse(url)
        if not all([parseada.scheme, parseada.netloc]):
            data['url'] = None
            data['short'] = None
            data['mensaje'] = u"""URL caca!"""
            data['clasemensaje'] = 'error'
        else:
            a = Atajo(url=url, user=usuario)
            data['short'] = a.slug()
            data['url'] = url
            # La probamos
            a.run_test()
            # Mensaje para el usuario de que el acortamiento
            # tuvo éxito.
            data['mensaje'] = u'''La URL
            <a href="%(url)s">%(url)s</a> se convirtió en:
            <a href="%(baseurl)s%(short)s">
            %(baseurl)s%(short)s</a>''' % data

            # Clase CSS que muestra las cosas como buenas
            data['clasemensaje'] = 'success'
    else:
        # No se acortó nada, no hay nada para mostrar.
        data['url'] = None
        data['short'] = None
        data['mensaje'] = None

    # Lista de atajos del usuario.
    data['atajos'] = Atajo.get(user=usuario)

    # Crear la página con esos datos.
    return data


@bottle.route('/:slug/edit')
@bottle.post('/:slug/edit')
@bottle.view('atajo.tpl')
def editar(slug):
    """Edita un slug"""
    if not 'REMOTE_USER' in bottle.request.environ:
        bottle.abort(401, "Sorry, access denied.")
    usuario = bottle.request.environ['REMOTE_USER'].decode('utf8')

    # Solo el dueño de un atajo puede editarlo
    a = Atajo.get(slug)
    # Atajo no existe o no sos el dueño
    if not a or a.user != usuario:
        bottle.abort(404, 'El atajo no existe')

    if 'url' in bottle.request.POST:
        # El usuario mandó el form
        a.url = bottle.request.POST['url'].decode('utf-8')
        a.activo = 'activo' in bottle.request.POST
        a.test = bottle.request.POST['test'].decode('utf-8')
        a.save()
        bottle.redirect('/')
    return {'atajo': a,
            'mensaje': '',
            }


@bottle.route('/:slug/del')
def borrar(slug):
    """Elimina un slug"""
    if not 'REMOTE_USER' in bottle.request.environ:
        bottle.abort(401, "Sorry, access denied.")
    usuario = bottle.request.environ['REMOTE_USER'].decode('utf8')
    # Solo el dueño de un atajo puede borrarlo
    a = Atajo.get(slug)
    if a and a.user == usuario:
        a.delete()
    # FIXME: pasar un mensaje en la sesión
    bottle.redirect('/')


@bottle.route('/:slug/test')
def run_test(slug):
    """Corre el test correspondiente a un atajo"""
    if not 'REMOTE_USER' in bottle.request.environ:
        bottle.abort(401, "Sorry, access denied.")
    usuario = bottle.request.environ['REMOTE_USER'].decode('utf8')

    # Solo el dueño de un atajo puede probarlo
    a = Atajo.get(slug)
    if a and a.user == usuario:
        a.run_test()
    # FIXME: pasar un mensaje en la sesión
    bottle.redirect('/%s/edit' % slug)


# Un slug está formado sólo por estos caracteres
@bottle.route('/:slug#[a-zA-Z0-9]+#')
def redir(slug):
    """Redirigir un slug"""
    # Buscamos el atajo correspondiente
    a = Atajo.get(slug=slug)
    if not a:
        bottle.abort(404, 'El atajo no existe')
    bottle.redirect(a.url)


@bottle.route('/static/:filename#.*#')
@bottle.route('/:filename#favicon.*#')
def static_file(filename):
    """Archivos estáticos (CSS etc)"""
    # No permitir volver para atras
    filename.replace("..", ".")
    # bottle.static_file parece no funcionar en esta version de bottle
    return open(os.path.join("static", *filename.split("/")))


if __name__ == '__main__':
    """Ejecutar con el server de debug de bottle"""
    bottle.debug(True)
    app = bottle.default_app()

    # Mostrar excepciones mientras desarrollamos
    app.catchall = False

    app = middleware(app,
        enable=True,
        setup_method='openid',
        openid_store_type='file',
        openid_template_file=os.path.join(os.getcwd(),
            'views', 'invitado.tpl'),
        openid_store_config=os.getcwd(),
        openid_path_signedin='/')

    app = AuthTKTMiddleware(SessionMiddleware(app),
                        'some auth ticket secret')

    # Inicializar DB
    Atajo.init_db()

    # Ejecutar aplicación
    bottle.run(app, host="0.0.0.0", port=80)
